package model.code.line;

import editor.ErrorHandler;
import event.TextReplacementEvent;
import lang.Language;
import model.code.CodeFileChunk;
import model.symbol.Scope;
import util.StringUtil;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;

/**
 * @author Neil Dickson
 */
public class MultiLineList extends ArrayList<Line> implements LineList<Line> {
	
	/**
	 * The {@link model.code.CodeFileChunk} containing this {@link LineList}
	 */
	private final CodeFileChunk	parent;
	private final int			locationType;
	private final int			locationIndex;

	private ArrayList<ActionListener>	observers;

	public MultiLineList(CodeFileChunk parent, int locationType) {
		this(parent,locationType,0);
	}
	public MultiLineList(CodeFileChunk parent, int locationType, int locationIndex) {
		this(parent,locationType,locationIndex,null);
	}
	/**
	 * This contructor is used in creating a {@link model.code.Function}, because it's not in a {@link model.code.CodeFile} yet.
	 * @param parent
	 * @param locationType
	 * @param language
	 */
	public MultiLineList(CodeFileChunk parent, int locationType,Language language) {
		this(parent,locationType,0,language);
	}

	public MultiLineList(CodeFileChunk parent, int locationType, int locationIndex, Language language) {
		this.parent = parent;
		this.locationType = locationType;
		this.locationIndex = locationIndex;
		observers = null;
		// There should always be at least one line
		if (language!=null) {
			add(language.createCodeLine(this,0,Line.EMPTY_CHAR_ARRAY));
		}
		else {
			add(createLine(0,Line.EMPTY_CHAR_ARRAY));
		}
	}

	public Scope getScope() {
		return parent.getScope();
	}
	public int getMode() {
		return parent.getMode();
	}
	public CodeFileChunk getParent() {
		return parent;
	}
	public int getLocationType() {
		return locationType;
	}
	public int getLocationIndex() {
		return locationIndex;
	}

	public int indexOf(Line element) {
		for (int i=0;i<size();++i) {
			if (element==get(i)) {
				return i;
			}
		}
		return -1;
	}

	public TextReplacementEvent replaceAll(String text) {
		return replace(0,0,size()-1,get(size()-1).length(),text);
	}

	public TextReplacementEvent insert(int line,int col,String text) {
		return replace(line,col,line,col,text);
	}

	public TextReplacementEvent replace(int startLine, int startCol, int endLine, int endCol, String text) {
		// Save the text being replaced
		String replacedText = getText(startLine,startCol,endLine,endCol);

		// Delete any fully-replaced lines
		while (startLine+1<endLine) {
			// NOTE: The indices are changing such that we always want to delete the line after startLine
			removeLine(startLine+1);
			--endLine;
		}
		if (endLine==startLine+1 && startCol==0) {
			removeLine(startLine);
			--endLine;
		}

		// Get lines of the replacement text
		String[] newLinesText = StringUtil.getLines(text);

		TextReplacementEvent event;

		try {
			// If replacing part of just 1 line
			if (startLine==endLine) {
				if (startCol==endCol && text.length()==0) {
					event = new TextReplacementEvent(this,replacedText,startLine,startCol,startLine,startCol);
				}
				else if (newLinesText.length==1) {
					event = get(startLine).replace(startCol,endCol,newLinesText[0]);
				}
				else {
					if (startCol>0 || endCol>0 || newLinesText[newLinesText.length-1].length()!=0) {
						// Split line into two, removing middle, replacing end of startLine and start of new endLine
						Line line = get(startLine);
						String lastPart = line.toString(endCol,line.length());
						line.replace(startCol,line.length(),newLinesText[0]);
						for (int i=1;i<newLinesText.length-1;++i) {
							Line newLine = createLine(startLine+i,newLinesText[i].toCharArray());
							addLine(newLine,startLine+i);
						}
						Line newLine = createLine(startLine+startLine+newLinesText.length-1,(newLinesText[newLinesText.length-1]+lastPart).toCharArray());
						addLine(newLine,startLine+newLinesText.length-1);
					}
					else {
						// Special case for moving full lines to the beginning of a line, fixing some bugs
						// Add all but the last line (since it's emtpy)
						for (int i=0;i<newLinesText.length-1;++i) {
							Line newLine = createLine(startLine+i,newLinesText[i].toCharArray());
							addLine(newLine,startLine+i);
						}
					}
					event = new TextReplacementEvent(this,replacedText,startLine,startCol,startLine+newLinesText.length-1,newLinesText[newLinesText.length-1].length());
				}
			}
			else {
				if (newLinesText.length==1) {
					String endText = newLinesText[0]+get(startLine+1).toString(endCol,get(startLine+1).length());
					removeLine(startLine+1);
					get(startLine).replace(startCol,get(startLine).length(),endText);
					event = new TextReplacementEvent(this,replacedText,startLine,startCol,startLine,startCol+newLinesText[0].length());
				}
				else {
					// Replacing end of startLine and start of endLine, adding lines in between
					get(startLine).replace(startCol,get(startLine).length(),newLinesText[0]);
					for (int i=1;i<newLinesText.length-1;++i) {
						Line newLine = createLine(startLine+i,newLinesText[i].toCharArray());
						addLine(newLine,startLine+i);
					}
					get(startLine+newLinesText.length-1).replace(0,endCol,newLinesText[newLinesText.length-1]);
					event = new TextReplacementEvent(this,replacedText,startLine,startCol,startLine+newLinesText.length-1,newLinesText[newLinesText.length-1].length());
				}
			}
		}
		catch (RuntimeException e) {
			ErrorHandler.showException(e,"PLEASE send in this message or a screenshot of it so that I can try to fix this elusive bug.\nPlease let me know what you were doing (probably undoing), and I'd recommend saving and restarting the IDE after that.\n\nSomething went wrong when replacing \""+replacedText+"\" at ("+startLine+","+startCol+") with \""+text+"\".");
			throw e;
		}
		notifyObservers();
		return event;
	}

	public String getText(int startLine,int startCol,int endLine,int endCol) {
		if (startLine==endLine) {
			if (startCol==endCol) {
				return "";	// This saves having to handle the case of getting no text from a non-existent first line
			}
			// TODO: Consider handling case of endCol==Integer.MAX_VALUE
			return get(startLine).toString(startCol,endCol);
		}
		else {
			StringBuffer buf = new StringBuffer(get(startLine).toString(startCol,Integer.MAX_VALUE)).append('\n');
			for (int i=startLine+1;i<endLine;++i) {
				buf.append(get(i).toString()).append('\n');
			}
			// TODO: Consider handling case of endCol==Integer.MAX_VALUE
			buf.append(get(endLine).toString(-1,endCol));
			return buf.toString();
		}
	}

	private void removeLine(int index) {
		// First, make sure everything is updated from the removal by making the line empty
		get(index).delete(0,get(index).length());
		// Then remove the empty line
		remove(index);
	}

	private void addLine(Line line,int index) {
		// First, the Line to occupy the space and make sure that the Line is in its LineList.
		add(index,line);
		// Then replace the whole line's text to update everything.
		line.replace(0,line.length(),line.toString());
	}

	public void addObserver(ActionListener observer) {
		if (observers==null) {
			observers = new ArrayList<ActionListener>(1);
		}
		if (!observers.contains(observer)) {
			observers.add(observer);
		}
	}
	public void removeObserver(ActionListener observer) {
		if (observers!=null) {
			observers.remove(observer);
		}
	}
	public void notifyObservers() {
		if (observers!=null) {
			ActionEvent e = new ActionEvent(this,ActionEvent.ACTION_PERFORMED,"");
			for (ActionListener observer : observers) {
				observer.actionPerformed(e);
			}
		}
	}

	public MultiLineList copy() {
		return copy(parent);
	}

	public MultiLineList copy(CodeFileChunk newParent) {
		MultiLineList that = new MultiLineList(newParent,locationType,locationIndex);
		that.remove(0);
		for (int i = 0; i<this.size(); i++) {
			that.add(this.get(i).copy(that,that.size()));
		}
		return that;
	}

	private Line createLine(int index,char[] text) {
		Language language;
		if (locationType==CodeFileChunk.LOCATION_CODE_PANEL && (language = parent.getLanguage())!=null) {
			return language.createCodeLine(this,index,text);
		}
		else {
			return new SimpleLine(this,text);
		}
	}

	public String toString() {
		StringBuffer buf = new StringBuffer();
		for (Line line : this) {
			buf.append(line.toString()).append('\n');
		}
		buf.setLength(Math.max(0,buf.length()-1));
		return buf.toString();
	}
}
